﻿using System;
using System.Collections.Generic;
using System.Disposables;
using System.IO;
using System.Runtime.InteropServices;

namespace RapidDownloader.Model
{
    public sealed class RarFileInfo
    {
        #region Properties

        public long BytesExtracted { get; set; }
        public bool ContinuedFromPrevious { get; set; }
        public bool ContinuedOnNext { get; set; }
        public int FileAttributes { get; set; }
        public uint FileCRC { get; set; }
        public string FileName { get; set; }
        public DateTime FileTime { get; set; }
        public int HostOS { get; set; }
        public bool IsDirectory { get; set; }
        public bool IsEncrypted { get; set; }
        public int Method { get; set; }
        public long PackedSize { get; set; }
        public long UnpackedSize { get; set; }
        public int VersionToUnpack { get; set; }

        public double PercentComplete
        {
            get { return UnpackedSize != 0L ? (((double)BytesExtracted) / UnpackedSize) * 100.0d : 0.0; }
        }

        #endregion
    }

    public class Unrar : IDisposable
    {
        #region PInvoke

        [DllImport("unrar.dll")]
        private static extern int RARCloseArchive(IntPtr hArcData);

        //[DllImport("unrar.dll")]
        //private static extern IntPtr RAROpenArchive(ref RarOpenArchiveData archiveData);

        [DllImport("UNRAR.DLL")]
        private static extern IntPtr RAROpenArchiveEx(ref RarOpenArchiveDataEx archiveData);

        //[DllImport("unrar.dll")]
        //private static extern int RARProcessFile(IntPtr hArcData, Operation operation, [MarshalAs(UnmanagedType.LPStr)] string destPath, [MarshalAs(UnmanagedType.LPStr)] string destName);

        [DllImport("unrar.dll", CharSet = CharSet.Unicode)]
        private static extern int RARProcessFileW(IntPtr hArcData, Operation operation, [MarshalAs(UnmanagedType.LPWStr)] string destPath, [MarshalAs(UnmanagedType.LPWStr)] string destName);

        //[DllImport("unrar.dll")]
        //private static extern int RARReadHeader(IntPtr hArcData, ref RARHeaderData headerData);

        [DllImport("unrar.dll")]
        private static extern int RARReadHeaderEx(IntPtr hArcData, ref RarHeaderDataEx headerData);

        [DllImport("unrar.dll")]
        private static extern void RARSetCallback(IntPtr hArcData, UnrarCallback callback, int userData);

        [DllImport("unrar.dll")]
        private static extern void RARSetPassword(IntPtr hArcData, [MarshalAs(UnmanagedType.LPStr)] string password);

        #endregion

        #region Fields

        private readonly UnrarCallback callback;
        private readonly bool retrieveComment;
        private IntPtr archiveHandle;
        private RarHeaderDataEx header;
        private string password;
        private readonly IDisposable closeArchive;

        private const int Fail = -1;
        private const int Success = 1;

        #endregion

        #region Events

        public event EventHandler<DataAvailableEventArgs> DataAvailable;
        public event EventHandler<ExtractionProgressEventArgs> ExtractionProgress;
        public event EventHandler<MissingVolumeEventArgs> MissingVolume;
        public event EventHandler<NewFileEventArgs> NewFile;
        public event EventHandler<NewVolumeEventArgs> NewVolume;
        public event EventHandler<PasswordRequiredEventArgs> PasswordRequired;

        #endregion

        #region Constructors

        public Unrar()
        {
            ArchivePathName = string.Empty;
            archiveHandle = IntPtr.Zero;
            retrieveComment = true;
            password = string.Empty;
            Comment = string.Empty;
            header = new RarHeaderDataEx();
            DestinationPath = string.Empty;
            callback = new UnrarCallback(RarCallback);
            closeArchive = Disposable.Create(Close);
        }

        public Unrar(string archivePathName)
            : this()
        {
            ArchivePathName = archivePathName;
        }

        ~Unrar()
        {
            Dispose();
        }

        #endregion

        #region Properties

        public ArchiveFlags ArchiveFlags { get; private set; }

        public string ArchivePathName { get; set; }

        public string Comment { get; private set; }

        public RarFileInfo CurrentFile { get; private set; }

        public string DestinationPath { get; set; }

        public string Password
        {
            get { return password; }
            set
            {
                password = value;
                if (archiveHandle != IntPtr.Zero)
                    RARSetPassword(archiveHandle, value);
            }
        }

        #endregion

        #region Public Methods

        public void Close()
        {
            if (archiveHandle != IntPtr.Zero)
            {
                var result = RARCloseArchive(archiveHandle);
                if (result != 0)
                    ProcessFileError(result);
                else
                    archiveHandle = IntPtr.Zero;
            }
        }

        public void Dispose()
        {
            if (archiveHandle != IntPtr.Zero)
            {
                RARCloseArchive(archiveHandle);
                archiveHandle = IntPtr.Zero;
            }
        }

        public void Extract()
        {
            Extract(DestinationPath, string.Empty);
        }

        public void Extract(string destinationName)
        {
            Extract(string.Empty, destinationName);
        }

        public void ExtractToDirectory(string destinationPath)
        {
            Extract(destinationPath, string.Empty);
        }

        public string[] ListFiles()
        {
            var list = new List<string>();
            while (ReadHeader())
            {
                if (!CurrentFile.IsDirectory)
                    list.Add(CurrentFile.FileName);
                Skip();
            }
            return list.ToArray();
        }

        public IDisposable Open()
        {
            if (string.IsNullOrWhiteSpace(ArchivePathName))
                throw new IOException("Archive name has not been set.");

            return Open(ArchivePathName, OpenMode.Extract);
        }

        public IDisposable Open(OpenMode openMode)
        {
            if (string.IsNullOrWhiteSpace(ArchivePathName))
                throw new IOException("Archive name has not been set.");

            return Open(ArchivePathName, openMode);
        }

        public IDisposable Open(string archivePathName, OpenMode openMode)
        {
            if (archiveHandle != IntPtr.Zero)
                Close();

            ArchivePathName = archivePathName;

            var archiveData = new RarOpenArchiveDataEx();
            archiveData.Initialize();
            archiveData.ArcName = ArchivePathName + "\0";
            archiveData.ArcNameW = ArchivePathName + "\0";
            archiveData.OpenMode = (uint)openMode;

            if (retrieveComment)
            {
                archiveData.CmtBuf = new string('\0', 0x10000);
                archiveData.CmtBufSize = 0x10000;
            }
            else
            {
                archiveData.CmtBuf = null;
                archiveData.CmtBufSize = 0;
            }
            var zero = RAROpenArchiveEx(ref archiveData);
            switch (archiveData.OpenResult)
            {
                case 11:
                    throw new OutOfMemoryException("Insufficient memory to perform operation.");

                case 12:
                    throw new IOException("Archive header broken");

                case 13:
                    throw new IOException("File is not a valid archive.");

                case 15:
                    throw new IOException("File could not be opened.");
            }
            archiveHandle = zero;
            ArchiveFlags = (ArchiveFlags)archiveData.Flags;
            RARSetCallback(archiveHandle, callback, GetHashCode());
            if (archiveData.CmtState == Success)
                Comment = archiveData.CmtBuf;
            if (password.Length != 0)
                RARSetPassword(archiveHandle, password);

            OnNewVolume(ArchivePathName);

            return closeArchive;
        }

        public bool ReadHeader()
        {
            if (archiveHandle == IntPtr.Zero)
                throw new IOException("Archive is not open.");

            header = new RarHeaderDataEx();
            header.Initialize();
            CurrentFile = null;
            switch (RARReadHeaderEx(archiveHandle, ref header))
            {
                case 10:
                    return false;

                case 12:
                    throw new IOException("Archive data is corrupt.");
            }
            if (((header.Flags & 1) != 0) && (CurrentFile != null))
                CurrentFile.ContinuedFromPrevious = true;
            else
            {
                CurrentFile = new RarFileInfo { FileName = header.FileNameW };
                if ((header.Flags & 2) != 0)
                    CurrentFile.ContinuedOnNext = true;
                CurrentFile.PackedSize = header.PackSizeHigh != 0 ? (header.PackSizeHigh * 0x100000000L) + header.PackSize : header.PackSize;
                CurrentFile.UnpackedSize = header.UnpSizeHigh != 0 ? (header.UnpSizeHigh * 0x100000000L) + header.UnpSize : header.UnpSize;
                CurrentFile.HostOS = (int)header.HostOS;
                CurrentFile.FileCRC = header.FileCRC;
                CurrentFile.FileTime = FromMsDosTime(header.FileTime);
                CurrentFile.VersionToUnpack = (int)header.UnpVer;
                CurrentFile.Method = (int)header.Method;
                CurrentFile.FileAttributes = (int)header.FileAttr;
                CurrentFile.BytesExtracted = 0L;
                if ((header.Flags & 0xe0) == 0xe0)
                    CurrentFile.IsDirectory = true;
                if ((header.Flags & 4) == 4)
                    CurrentFile.IsEncrypted = true;
                OnNewFile();
            }
            return true;
        }

        public void Skip()
        {
            var result = RARProcessFileW(archiveHandle, Operation.Skip, string.Empty, string.Empty);
            if (result != 0)
                ProcessFileError(result);
        }

        public void Test()
        {
            var result = RARProcessFileW(archiveHandle, Operation.Test, string.Empty, string.Empty);
            if (result != 0)
                ProcessFileError(result);
        }

        #endregion

        #region Protected Methods

        protected virtual int OnDataAvailable(IntPtr bytes, int bytesLength)
        {
            if (CurrentFile != null)
                CurrentFile.BytesExtracted += bytesLength;

            var handler = DataAvailable;
            if (handler != null)
            {
                var buffer = new byte[bytesLength];
                Marshal.Copy(bytes, buffer, 0, bytesLength);
                var e = new DataAvailableEventArgs(buffer);
                handler(this, e);
                if (!e.ContinueOperation)
                    return Fail;
            }

            var progressHandler = ExtractionProgress;
            if (progressHandler != null && CurrentFile != null)
            {
                var progressArgs = new ExtractionProgressEventArgs
                                {
                                    FileName = CurrentFile.FileName,
                                    FileSize = CurrentFile.UnpackedSize,
                                    BytesExtracted = CurrentFile.BytesExtracted,
                                    PercentComplete = CurrentFile.PercentComplete
                                };
                progressHandler(this, progressArgs);
                if (!progressArgs.ContinueOperation)
                    return Fail;
            }

            return Success;
        }

        protected virtual string OnMissingVolume(string volume)
        {
            var volumeName = string.Empty;
            var handler = MissingVolume;
            if (handler != null)
            {
                var e = new MissingVolumeEventArgs(volume);
                handler(this, e);
                if (e.ContinueOperation)
                    volumeName = e.VolumeName;
            }
            return volumeName;
        }

        protected virtual void OnNewFile()
        {
            var handler = NewFile;
            if (handler != null)
                handler(this, new NewFileEventArgs(CurrentFile));
        }

        protected virtual int OnNewVolume(string volume)
        {
            var handler = NewVolume;
            if (handler != null)
            {
                var e = new NewVolumeEventArgs(volume);
                handler(this, e);
                if (!e.ContinueOperation)
                    return Fail;
            }
            return Success;
        }

        protected virtual int OnPasswordRequired(IntPtr p1, int p2)
        {
            var handler = PasswordRequired;
            if (handler == null)
                throw new IOException("Password is required for extraction.");

            var e = new PasswordRequiredEventArgs();
            handler(this, e);
            if (!e.ContinueOperation || (e.Password.Length <= 0))
                return Fail;
            for (var i = 0; (i < e.Password.Length) && (i < p2); i++)
                Marshal.WriteByte(p1, i, (byte)e.Password[i]);
            Marshal.WriteByte(p1, e.Password.Length, 0);
            return Success;
        }

        #endregion

        #region Private Methods

        private void Extract(string destinationPath, string destinationName)
        {
            var result = RARProcessFileW(archiveHandle, Operation.Extract, destinationPath, destinationName);
            if (result != 0)
                ProcessFileError(result);
        }

        private int RarCallback(uint msg, int userData, IntPtr p1, int p2)
        {
            switch ((CallbackMessages)msg)
            {
                case CallbackMessages.VolumeChange:
                    var volume = Marshal.PtrToStringAnsi(p1);
                    if (p2 != 1)
                    {
                        if (p2 != 0)
                            return Fail;

                        var newVolumeName = OnMissingVolume(volume);
                        if (string.IsNullOrWhiteSpace(newVolumeName))
                            return Fail;

                        if (newVolumeName != volume)
                        {
                            for (var i = 0; i < newVolumeName.Length; i++)
                                Marshal.WriteByte(p1, i, (byte)newVolumeName[i]);
                            Marshal.WriteByte(p1, newVolumeName.Length, 0);
                        }
                        return Success;
                    }
                    return OnNewVolume(volume);

                case CallbackMessages.ProcessData:
                    return OnDataAvailable(p1, p2);

                case CallbackMessages.NeedPassword:
                    return OnPasswordRequired(p1, p2);
            }
            return Fail;
        }

        #endregion

        #region Class Methods

        private static DateTime FromMsDosTime(uint dosTime)
        {
            var num7 = (ushort)((dosTime & -65536) >> 0x10);
            var num8 = (ushort)(dosTime & 0xffff);
            var year = ((num7 & 0xfe00) >> 9) + 0x7bc;
            var month = (num7 & 480) >> 5;
            var day = num7 & 0x1f;
            var hour = (num8 & 0xf800) >> 11;
            var minute = (num8 & 0x7e0) >> 5;
            return new DateTime(year, month, day, hour, minute, (num8 & 0x1f) << 1);
        }

        private static void ProcessFileError(int result)
        {
            switch (((RarError)result))
            {
                case RarError.BadData:
                    throw new IOException("File CRC Error");

                case RarError.BadArchive:
                    throw new IOException("File is not a valid archive.");

                case RarError.UnknownFormat:
                    throw new OutOfMemoryException("Unknown archive format.");

                case RarError.OpenError:
                    throw new IOException("File could not be opened.");

                case RarError.CreateError:
                    throw new IOException("File could not be created.");

                case RarError.CloseError:
                    throw new IOException("File close error.");

                case RarError.ReadError:
                    throw new IOException("File read error.");

                case RarError.WriteError:
                    throw new IOException("File write error.");
            }
        }

        #endregion

        #region Nested Types

        private enum CallbackMessages : uint
        {
            NeedPassword = 2,
            ProcessData = 1,
            VolumeChange = 0
        }

        private enum Operation : uint
        {
            Extract = 2,
            Skip = 0,
            Test = 1
        }

        //[StructLayout(LayoutKind.Sequential)]
        //private struct RARHeaderData
        //{
        //    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        //    public readonly string ArcName;
        //    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        //    public readonly string FileName;
        //    public readonly uint Flags;
        //    public readonly uint PackSize;
        //    public readonly uint UnpSize;
        //    public readonly uint HostOS;
        //    public readonly uint FileCRC;
        //    public readonly uint FileTime;
        //    public readonly uint UnpVer;
        //    public readonly uint Method;
        //    public readonly uint FileAttr;
        //    [MarshalAs(UnmanagedType.LPStr)]
        //    public string CmtBuf;
        //    public uint CmtBufSize;
        //    public readonly uint CmtSize;
        //    public readonly uint CmtState;
        //    public void Initialize()
        //    {
        //        CmtBuf = new string('\0', 0x10000);
        //        CmtBufSize = 0x10000;
        //    }
        //}

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct RarHeaderDataEx
        {
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x200)]
            public string ArcName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x400)]
            public string ArcNameW;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x200)]
            public string FileName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x400)]
            public string FileNameW;
            public uint Flags;
            public uint PackSize;
            public uint PackSizeHigh;
            public uint UnpSize;
            public uint UnpSizeHigh;
            public uint HostOS;
            public uint FileCRC;
            public uint FileTime;
            public uint UnpVer;
            public uint Method;
            public uint FileAttr;
            [MarshalAs(UnmanagedType.LPStr)]
            public string CmtBuf;
            public uint CmtBufSize;
            public uint CmtSize;
            public uint CmtState;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x400)]
            public uint[] Reserved;

            public void Initialize()
            {
                CmtBuf = new string('\0', 0x10000);
                CmtBufSize = 0x10000;
            }
        }

        //[StructLayout(LayoutKind.Sequential)]
        //private struct RarOpenArchiveData
        //{
        //    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        //    public string ArcName;
        //    public uint OpenMode;
        //    public uint OpenResult;
        //    [MarshalAs(UnmanagedType.LPStr)]
        //    public string CmtBuf;
        //    public uint CmtBufSize;
        //    public uint CmtSize;
        //    public uint CmtState;
        //    public void Initialize()
        //    {
        //        CmtBuf = new string('\0', 0x10000);
        //        CmtBufSize = 0x10000;
        //    }
        //}

        [StructLayout(LayoutKind.Sequential)]
        private struct RarOpenArchiveDataEx
        {
            [MarshalAs(UnmanagedType.LPStr)]
            public string ArcName;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string ArcNameW;
            public uint OpenMode;
            public uint OpenResult;
            [MarshalAs(UnmanagedType.LPStr)]
            public string CmtBuf;
            public uint CmtBufSize;
            public uint CmtSize;
            public uint CmtState;
            public uint Flags;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x20)]
            public uint[] Reserved;

            public void Initialize()
            {
                CmtBuf = new string('\0', 0x10000);
                CmtBufSize = 0x10000;
                Reserved = new uint[0x20];
            }
        }

        private enum RarError : uint
        {
            BadArchive = 13,
            BadData = 12,
            //BufferTooSmall = 20,
            CloseError = 0x11,
            CreateError = 0x10,
            //EndOfArchive = 10,
            //InsufficientMemory = 11,
            OpenError = 15,
            ReadError = 0x12,
            //UnknownError = 0x15,
            UnknownFormat = 14,
            WriteError = 0x13
        }

        private delegate int UnrarCallback(uint msg, int userData, IntPtr p1, int p2);

        //private enum VolumeMessage : uint
        //{
        //    Ask = 0,
        //    Notify = 1
        //}

        #endregion
    }

    public enum OpenMode
    {
        List,
        Extract
    }

    [Flags]
    public enum ArchiveFlags : uint
    {
        AuthenticityPresent = 0x20,
        CommentPresent = 2,
        EncryptedHeaders = 0x80,
        FirstVolume = 0x100,
        Lock = 4,
        NewNamingScheme = 0x10,
        RecoveryRecordPresent = 0x40,
        SolidArchive = 8,
        Volume = 1
    }

    public class DataAvailableEventArgs : EventArgs
    {
        public bool ContinueOperation = true;
        public readonly byte[] Data;

        public DataAvailableEventArgs(byte[] data)
        {
            Data = data;
        }
    }

    public class PasswordRequiredEventArgs : EventArgs
    {
        public bool ContinueOperation = true;
        public string Password = string.Empty;
    }

    public class NewVolumeEventArgs : EventArgs
    {
        public bool ContinueOperation = true;
        public string VolumeName;

        public NewVolumeEventArgs(string volumeName)
        {
            VolumeName = volumeName;
        }
    }

    public class NewFileEventArgs : EventArgs
    {
        public RarFileInfo FileInfo { get; private set; }

        public NewFileEventArgs(RarFileInfo fileInfo)
        {
            FileInfo = fileInfo;
        }
    }

    public class MissingVolumeEventArgs : EventArgs
    {
        public bool ContinueOperation;
        public string VolumeName;

        public MissingVolumeEventArgs(string volumeName)
        {
            VolumeName = volumeName;
        }
    }

    public class ExtractionProgressEventArgs : EventArgs
    {
        public long BytesExtracted;
        public bool ContinueOperation = true;
        public string FileName;
        public long FileSize;
        public double PercentComplete;
    }
}